{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Marly - Data Loading Workflow\n",
    "\n",
    "This notebook will demonstrate how Marly can be used together with a popular AI agent framework like LangGraph to perform a data loading workflow. LangGraph will be used to orchestrate collaboration amongst the agents while Marly will be used for data processing. In the below example workflow a processor and display table agent will collaborate to perform the task of retrieving a pdf file, processing it, loading into a SQL Database and then understanding the results\n",
    "\n",
    "The resulting graph will be something like this:\n",
    "\n",
    "![Alt text](./wkflow.jpeg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core marly python-dotenv\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "\n",
    "def _set_if_undefined(var: str):\n",
    "    if not os.environ.get(var):\n",
    "        os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
    "\n",
    "\n",
    "_set_if_undefined(\"OPENAI_API_KEY\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Defining Agent Components"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import (\n",
    "    BaseMessage,\n",
    "    HumanMessage,\n",
    "    ToolMessage,\n",
    ")\n",
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "\n",
    "from langgraph.graph import END, StateGraph, START\n",
    "\n",
    "\n",
    "def create_agent(llm, tools, system_message: str):\n",
    "    \"\"\"Create an agent.\"\"\"\n",
    "    prompt = ChatPromptTemplate.from_messages(\n",
    "        [\n",
    "            (\n",
    "                \"system\",\n",
    "                \"You are a helpful AI assistant, collaborating with other assistants.\"\n",
    "                \" Use the provided tools to progress towards answering the question.\"\n",
    "                \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n",
    "                \" will help where you left off. Execute what you can to make progress.\"\n",
    "                \" If you or any of the other assistants have the final answer or deliverable,\"\n",
    "                \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n",
    "                \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n",
    "            ),\n",
    "            MessagesPlaceholder(variable_name=\"messages\"),\n",
    "        ]\n",
    "    )\n",
    "    prompt = prompt.partial(system_message=system_message)\n",
    "    prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n",
    "    return prompt | llm.bind_tools(tools)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Defining Tools"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 81,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.tools import tool\n",
    "from marly import Marly\n",
    "from dotenv import load_dotenv\n",
    "import time\n",
    "import json\n",
    "import os\n",
    "import logging\n",
    "import base64\n",
    "import zlib\n",
    "import sqlite3\n",
    "import pandas as pd\n",
    "\n",
    "load_dotenv()\n",
    "\n",
    "BASE_URL = \"http://localhost:8100\"\n",
    "PDF_FILE_PATH = \"../example_files/lacers_reduced.pdf\"\n",
    "CLIENT = Marly(base_url=BASE_URL)\n",
    "\n",
    "# Define schema for Marly, table column name and a brief description of the column\n",
    "SCHEMA_1 = {\n",
    "        \"Firm\": \"The name of the firm\",\n",
    "        \"Number of Funds\": \"The number of funds managed by the firm\",\n",
    "        \"Commitment\": \"The commitment amount in millions of dollars\",\n",
    "        \"Percent of Total Comm\": \"The percentage of total commitment\",\n",
    "        \"Exposure (FMV + Unfunded)\": \"The exposure including fair market value and unfunded commitments in millions of dollars\",\n",
    "        \"Percent of Total Exposure\": \"The percentage of total exposure\",\n",
    "        \"TVPI\": \"Total Value to Paid-In multiple\",\n",
    "        \"Net IRR\": \"Net Internal Rate of Return as a percentage\"\n",
    "    }\n",
    "\n",
    "# Helper function to read and encode the pdf file\n",
    "def read_and_encode_pdf(file_path):\n",
    "    with open(file_path, \"rb\") as file:\n",
    "        pdf_content = base64.b64encode(zlib.compress(file.read())).decode('utf-8')\n",
    "    logging.debug(f\"{file_path} read and encoded\")\n",
    "    return pdf_content\n",
    "\n",
    "def display_table():\n",
    "    try:\n",
    "        # Connect to the SQLite database\n",
    "        conn = sqlite3.connect('etl_workflow.db')\n",
    "        \n",
    "        # Use pandas to read the table into a DataFrame\n",
    "        df = pd.read_sql_query(\"SELECT * FROM private_equity_firms\", conn)\n",
    "        \n",
    "        # Close the connection\n",
    "        conn.close()\n",
    "        \n",
    "        # Print the DataFrame in a pretty format\n",
    "        print(df.to_string(index=False))\n",
    "    except Exception as e:\n",
    "        print(f\"Error displaying table: {str(e)}\")\n",
    "\n",
    "# Main function to process the pdf file\n",
    "def process_pdf(file_path):\n",
    "    \"\"\" Processes a PDF file using Marly and returns a JSON string.\"\"\"\n",
    "    pdf_content = read_and_encode_pdf(file_path)\n",
    "\n",
    "    try:\n",
    "        pipeline_response = CLIENT.pipelines.create(\n",
    "            api_key=os.getenv(\"CEREBRAS_API_KEY\"),\n",
    "            provider_model_name=\"llama3.1-70b\",\n",
    "            provider_type=\"cerebras\",\n",
    "            workloads=[{\"pdf_stream\": pdf_content, \"schemas\": [json.dumps(SCHEMA_1)]}],\n",
    "        )\n",
    "\n",
    "        while True:\n",
    "            results = CLIENT.pipelines.retrieve(pipeline_response.task_id)\n",
    "            if results.status == 'COMPLETED':\n",
    "                return [json.loads(results.results[0].metrics[f'schema_{i}']) for i in range(len(results.results[0].metrics))]\n",
    "            elif results.status == 'FAILED':\n",
    "                return None\n",
    "            time.sleep(15)\n",
    "\n",
    "    except Exception as e:\n",
    "        logging.error(f\"Error in pipeline process: {e}\")\n",
    "        return None\n",
    "\n",
    "def load_data(input_data) -> str:\n",
    "    try:\n",
    "        # Debug: Print the input_data type and content\n",
    "        print(f\"Input Data Type: {type(input_data)}\")\n",
    "        print(f\"Input Data Content: {input_data}\")\n",
    "\n",
    "        # Ensure input_data is a list\n",
    "        if not isinstance(input_data, list):\n",
    "            return \"Unsupported input data format: input_data is not a list.\"\n",
    "\n",
    "        # Case 1: List contains a single dictionary with lists as values\n",
    "        if (len(input_data) == 1 and isinstance(input_data[0], dict) and \n",
    "            all(isinstance(v, list) for v in input_data[0].values())):\n",
    "            input_dict = input_data[0]\n",
    "            keys = input_dict.keys()\n",
    "\n",
    "            # Check that all lists have the same length\n",
    "            list_lengths = [len(v) for v in input_dict.values()]\n",
    "            if len(set(list_lengths)) != 1:\n",
    "                return \"All lists in the input dictionary must have the same length.\"\n",
    "\n",
    "            # Transform the input data into a list of dictionaries\n",
    "            data = [dict(zip(keys, values)) for values in zip(*input_dict.values())]\n",
    "            print(f\"Transformed Data: {data}\")\n",
    "\n",
    "        # Case 2: List contains multiple dictionaries with scalar values\n",
    "        elif all(isinstance(item, dict) for item in input_data):\n",
    "            data = input_data\n",
    "            print(f\"Data is a list of dictionaries: {data}\")\n",
    "\n",
    "        else:\n",
    "            return \"Unsupported input data format.\"\n",
    "\n",
    "        # Connect to the SQLite database\n",
    "        conn = sqlite3.connect('etl_workflow.db')\n",
    "        cursor = conn.cursor()\n",
    "\n",
    "        # Create the table if it doesn't exist\n",
    "        cursor.execute('''\n",
    "            CREATE TABLE IF NOT EXISTS private_equity_firms (\n",
    "                Firm TEXT PRIMARY KEY,\n",
    "                Number_of_Funds TEXT,\n",
    "                Commitment TEXT,\n",
    "                Percent_of_Total_Comm TEXT,\n",
    "                Exposure TEXT,\n",
    "                Percent_of_Total_Exposure TEXT,\n",
    "                TVPI TEXT,\n",
    "                Net_IRR TEXT\n",
    "            )\n",
    "        ''')\n",
    "        print(\"Database table 'private_equity_firms' is ready.\")\n",
    "\n",
    "        # Insert or replace the data\n",
    "        for firm in data:\n",
    "            cursor.execute('''\n",
    "                INSERT OR REPLACE INTO private_equity_firms \n",
    "                (Firm, Number_of_Funds, Commitment, Percent_of_Total_Comm, Exposure, Percent_of_Total_Exposure, TVPI, Net_IRR) \n",
    "                VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n",
    "            ''', (\n",
    "                firm.get('Firm', ''),\n",
    "                firm.get('Number of Funds', ''),\n",
    "                firm.get('Commitment', ''),\n",
    "                firm.get('Percent of Total Comm', ''),\n",
    "                firm.get('Exposure (FMV + Unfunded)', ''),\n",
    "                firm.get('Percent of Total Exposure', ''),\n",
    "                firm.get('TVPI', ''),\n",
    "                firm.get('Net IRR', '')\n",
    "            ))\n",
    "            print(f\"Inserted/Updated Firm: {firm.get('Firm', '')}\")\n",
    "\n",
    "        # Commit the changes and close the connection\n",
    "        conn.commit()\n",
    "        conn.close()\n",
    "\n",
    "        return f\"Successfully loaded {len(data)} records into the database.\"\n",
    "\n",
    "    except Exception as e:\n",
    "        return f\"Error loading data into SQLite database: {str(e)}\"\n",
    "\n",
    "# Define the tool to process the pdf file\n",
    "@tool\n",
    "def process_pdf_tool(file_path: str) -> str:\n",
    "    \"\"\"Process a PDF file and return a string.\"\"\"\n",
    "    results = process_pdf(file_path)\n",
    "    print(results)\n",
    "    return load_data(results)\n",
    "\n",
    "@tool\n",
    "def display_table_tool() -> str:\n",
    "    \"\"\"Display the table.\"\"\"\n",
    "    return display_table()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [],
   "source": [
    "import operator\n",
    "from typing import Annotated, Sequence, TypedDict\n",
    "\n",
    "\n",
    "# This defines the object that is passed between each node\n",
    "# in the graph. We will create different nodes for each agent and tool\n",
    "class AgentState(TypedDict):\n",
    "    messages: Annotated[Sequence[BaseMessage], operator.add]\n",
    "    sender: str"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "outputs": [],
   "source": [
    "import functools\n",
    "\n",
    "from langchain_core.messages import AIMessage\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "llm = ChatOpenAI(model=\"gpt-4o\")\n",
    "\n",
    "# Helper function to create a node for a given agent\n",
    "def agent_node(state, agent, name):\n",
    "    result = agent.invoke(state)\n",
    "    # We convert the agent output into a format that is suitable to append to the global state\n",
    "    if isinstance(result, ToolMessage):\n",
    "        pass\n",
    "    else:\n",
    "        result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n",
    "    return {\n",
    "        \"messages\": [result],\n",
    "        \"sender\": name,\n",
    "    }\n",
    "\n",
    "processor = create_agent(\n",
    "    llm,\n",
    "    [process_pdf_tool],\n",
    "    system_message=\"You should process the PDF and return the status of the data loading process.\",\n",
    ")\n",
    "processor_node = functools.partial(agent_node, agent=processor, name=\"processor\")\n",
    "\n",
    "\n",
    "table_display = create_agent(\n",
    "    llm,\n",
    "    [display_table_tool],\n",
    "    system_message=\"After the ETL process is complete, display the table.\",\n",
    ")\n",
    "table_display_node = functools.partial(agent_node, agent=table_display, name=\"table_display\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.prebuilt import ToolNode\n",
    "\n",
    "tools = [process_pdf_tool, display_table_tool]\n",
    "tool_node = ToolNode(tools)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal\n",
    "\n",
    "\n",
    "def router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n",
    "    # This is the router\n",
    "    messages = state[\"messages\"]\n",
    "    last_message = messages[-1]\n",
    "    if last_message.tool_calls:\n",
    "        # The previous agent is invoking a tool\n",
    "        return \"call_tool\"\n",
    "    if \"FINAL ANSWER\" in last_message.content:\n",
    "        # Any agent decided the work is done\n",
    "        return \"__end__\"\n",
    "    return \"continue\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "outputs": [],
   "source": [
    "workflow = StateGraph(AgentState)\n",
    "\n",
    "# Add nodes for processor and loader\n",
    "workflow.add_node(\"processor\", processor_node)\n",
    "workflow.add_node(\"table_display\", table_display_node) \n",
    "workflow.add_node(\"call_tool\", tool_node)\n",
    "\n",
    "# Add conditional edges for processor\n",
    "workflow.add_conditional_edges(\n",
    "    \"processor\",\n",
    "    router,\n",
    "    {\"continue\": \"table_display\", \"call_tool\": \"call_tool\", \"__end__\": END},\n",
    ")\n",
    "\n",
    "# Add conditional edges for loader\n",
    "workflow.add_conditional_edges(\n",
    "    \"table_display\",\n",
    "    router,\n",
    "    {\"continue\": END, \"call_tool\": \"call_tool\", \"__end__\": END},\n",
    ")\n",
    "\n",
    "# Add conditional edges for call_tool\n",
    "workflow.add_conditional_edges(\n",
    "    \"call_tool\",\n",
    "    lambda x: x[\"sender\"],\n",
    "    {\n",
    "        \"processor\": \"processor\",\n",
    "        \"table_display\": \"table_display\",\n",
    "    },\n",
    ")\n",
    "\n",
    "# Set the starting node\n",
    "workflow.add_edge(START, \"processor\")\n",
    "\n",
    "# Compile the graph\n",
    "graph = workflow.compile()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 86,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "try:\n",
    "    display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
    "except Exception:\n",
    "    # This requires some extra dependencies and is optional\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 82,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'processor': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_0GGkx4M1LyBgCR2QRL0pdElm', 'function': {'arguments': '{\"file_path\":\"../example_files/lacers_reduced.pdf\"}', 'name': 'process_pdf_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 24, 'prompt_tokens': 402, 'total_tokens': 426, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_52a7f40b0b', 'finish_reason': 'tool_calls', 'logprobs': None}, name='processor', id='run-3216336a-76c0-43bd-b6b5-fb0ceddafbb0-0', tool_calls=[{'name': 'process_pdf_tool', 'args': {'file_path': '../example_files/lacers_reduced.pdf'}, 'id': 'call_0GGkx4M1LyBgCR2QRL0pdElm', 'type': 'tool_call'}], usage_metadata={'input_tokens': 402, 'output_tokens': 24, 'total_tokens': 426})], 'sender': 'processor'}}\n",
      "----\n",
      "Input Data Type: <class 'list'>\n",
      "Input Data Content: [{'Firm': ['Spark Capital', 'Genstar Partners', 'Gilde Buyout Partners', 'Harvest Partners', 'DEFY', 'TA Associates', 'Advent International', 'Freeman Spogli', 'Platinum Equity', 'NEA', 'P4G', 'Oak HC-FT', 'Sunstone', 'Montagu Private Equity', 'KPS', 'Clearlake Capital'], 'Number of Funds': ['-', '-', '-', '-', '-', '-', '5', '-', '4', '4', '-', '-', '-', '-', '-', '-'], 'Commitment': ['13.3', '50.0', '34.93', '50.0', '18.0', '35.0', '45.0', '25.0', '50.0', '35.0', '10.0', '25.0', '10.0', '35.44', '40.0', '30.0'], 'Percent of Total Comm': ['-', '-', '-', '-', '-', '-', '2.8', '-', '2.2', '1.9', '-', '-', '-', '-', '-', '-'], 'Exposure (FMV + Unfunded)': ['-', '-', '-', '-', '-', '-', '132320692', '-', '105732861', '103565654', '-', '-', '-', '-', '-', '-'], 'Percent of Total Exposure': ['-', '-', '-', '-', '-', '-', '3.7', '-', '3.0', '2.9', '-', '-', '-', '-', '-', '-'], 'TVPI': ['-', '-', '-', '-', '-', '-', '1.64', '-', '1.64', '1.60', '-', '-', '-', '-', '-', '-'], 'Net IRR': ['-', '-', '-', '-', '-', '-', '16.0', '-', '28.5', '15.7', '-', '-', '-', '-', '-', '-']}]\n",
      "Transformed Data: [{'Firm': 'Spark Capital', 'Number of Funds': '-', 'Commitment': '13.3', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Genstar Partners', 'Number of Funds': '-', 'Commitment': '50.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Gilde Buyout Partners', 'Number of Funds': '-', 'Commitment': '34.93', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Harvest Partners', 'Number of Funds': '-', 'Commitment': '50.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'DEFY', 'Number of Funds': '-', 'Commitment': '18.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'TA Associates', 'Number of Funds': '-', 'Commitment': '35.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Advent International', 'Number of Funds': '5', 'Commitment': '45.0', 'Percent of Total Comm': '2.8', 'Exposure (FMV + Unfunded)': '132320692', 'Percent of Total Exposure': '3.7', 'TVPI': '1.64', 'Net IRR': '16.0'}, {'Firm': 'Freeman Spogli', 'Number of Funds': '-', 'Commitment': '25.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Platinum Equity', 'Number of Funds': '4', 'Commitment': '50.0', 'Percent of Total Comm': '2.2', 'Exposure (FMV + Unfunded)': '105732861', 'Percent of Total Exposure': '3.0', 'TVPI': '1.64', 'Net IRR': '28.5'}, {'Firm': 'NEA', 'Number of Funds': '4', 'Commitment': '35.0', 'Percent of Total Comm': '1.9', 'Exposure (FMV + Unfunded)': '103565654', 'Percent of Total Exposure': '2.9', 'TVPI': '1.60', 'Net IRR': '15.7'}, {'Firm': 'P4G', 'Number of Funds': '-', 'Commitment': '10.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Oak HC-FT', 'Number of Funds': '-', 'Commitment': '25.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Sunstone', 'Number of Funds': '-', 'Commitment': '10.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Montagu Private Equity', 'Number of Funds': '-', 'Commitment': '35.44', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'KPS', 'Number of Funds': '-', 'Commitment': '40.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}, {'Firm': 'Clearlake Capital', 'Number of Funds': '-', 'Commitment': '30.0', 'Percent of Total Comm': '-', 'Exposure (FMV + Unfunded)': '-', 'Percent of Total Exposure': '-', 'TVPI': '-', 'Net IRR': '-'}]\n",
      "Database table 'private_equity_firms' is ready.\n",
      "Inserted/Updated Firm: Spark Capital\n",
      "Inserted/Updated Firm: Genstar Partners\n",
      "Inserted/Updated Firm: Gilde Buyout Partners\n",
      "Inserted/Updated Firm: Harvest Partners\n",
      "Inserted/Updated Firm: DEFY\n",
      "Inserted/Updated Firm: TA Associates\n",
      "Inserted/Updated Firm: Advent International\n",
      "Inserted/Updated Firm: Freeman Spogli\n",
      "Inserted/Updated Firm: Platinum Equity\n",
      "Inserted/Updated Firm: NEA\n",
      "Inserted/Updated Firm: P4G\n",
      "Inserted/Updated Firm: Oak HC-FT\n",
      "Inserted/Updated Firm: Sunstone\n",
      "Inserted/Updated Firm: Montagu Private Equity\n",
      "Inserted/Updated Firm: KPS\n",
      "Inserted/Updated Firm: Clearlake Capital\n",
      "{'call_tool': {'messages': [ToolMessage(content='Successfully loaded 16 records into the database.', name='process_pdf_tool', tool_call_id='call_0GGkx4M1LyBgCR2QRL0pdElm')]}}\n",
      "----\n",
      "{'processor': {'messages': [AIMessage(content='ETL process complete.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 6, 'prompt_tokens': 446, 'total_tokens': 452, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_52a7f40b0b', 'finish_reason': 'stop', 'logprobs': None}, name='processor', id='run-a3ca9434-415f-4dac-805b-dcb1ba216578-0', usage_metadata={'input_tokens': 446, 'output_tokens': 6, 'total_tokens': 452})], 'sender': 'processor'}}\n",
      "----\n",
      "{'table_display': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_fihskGyH2NZtRSbO87qt6707', 'function': {'arguments': '{}', 'name': 'display_table_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 442, 'total_tokens': 453, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_e375328146', 'finish_reason': 'tool_calls', 'logprobs': None}, name='table_display', id='run-e4bd0c08-0b5d-4638-83f1-ab75cd75d195-0', tool_calls=[{'name': 'display_table_tool', 'args': {}, 'id': 'call_fihskGyH2NZtRSbO87qt6707', 'type': 'tool_call'}], usage_metadata={'input_tokens': 442, 'output_tokens': 11, 'total_tokens': 453})], 'sender': 'table_display'}}\n",
      "----\n",
      "                  Firm Number_of_Funds Commitment Percent_of_Total_Comm  Exposure Percent_of_Total_Exposure TVPI Net_IRR\n",
      "         Spark Capital               -       13.3                     -         -                         -    -       -\n",
      "      Genstar Partners               -       50.0                     -         -                         -    -       -\n",
      " Gilde Buyout Partners               -      34.93                     -         -                         -    -       -\n",
      "      Harvest Partners               -       50.0                     -         -                         -    -       -\n",
      "                  DEFY               -       18.0                     -         -                         -    -       -\n",
      "         TA Associates               -       35.0                     -         -                         -    -       -\n",
      "  Advent International               5       45.0                   2.8 132320692                       3.7 1.64    16.0\n",
      "        Freeman Spogli               -       25.0                     -         -                         -    -       -\n",
      "       Platinum Equity               4       50.0                   2.2 105732861                       3.0 1.64    28.5\n",
      "                   NEA               4       35.0                   1.9 103565654                       2.9 1.60    15.7\n",
      "                   P4G               -       10.0                     -         -                         -    -       -\n",
      "             Oak HC-FT               -       25.0                     -         -                         -    -       -\n",
      "              Sunstone               -       10.0                     -         -                         -    -       -\n",
      "Montagu Private Equity               -      35.44                     -         -                         -    -       -\n",
      "                   KPS               -       40.0                     -         -                         -    -       -\n",
      "     Clearlake Capital               -       30.0                     -         -                         -    -       -\n",
      "{'call_tool': {'messages': [ToolMessage(content='null', name='display_table_tool', tool_call_id='call_fihskGyH2NZtRSbO87qt6707')]}}\n",
      "----\n",
      "{'table_display': {'messages': [AIMessage(content='ETL process complete.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 6, 'prompt_tokens': 466, 'total_tokens': 472, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_e375328146', 'finish_reason': 'stop', 'logprobs': None}, name='table_display', id='run-cbcc29e3-3ffe-48f5-ab8a-2bcc16d5e6cf-0', usage_metadata={'input_tokens': 466, 'output_tokens': 6, 'total_tokens': 472})], 'sender': 'table_display'}}\n",
      "----\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "\n",
    "events = graph.stream(\n",
    "    {\n",
    "        \"messages\": [\n",
    "            HumanMessage(\n",
    "                content=f\"\"\"Please perform the following two-step process:\n",
    "\n",
    "                1. Processor:\n",
    "                - Extract data from the PDF file located at: {PDF_FILE_PATH}\n",
    "                - Use the following schema to guide your processing:\n",
    "                    {json.dumps(SCHEMA_1, indent=2)}\n",
    "                - Use the process_pdf_tool to process the PDF file to extract the data according to the schema.\n",
    "\n",
    "                2. Table Display:\n",
    "                - Display the table after the processor has processed the PDF file.\n",
    "\n",
    "                Please execute these steps in order!\n",
    "    \n",
    "                Once you've completed both steps, finish by saying \"ETL process complete.\"\n",
    "                \"\"\"\n",
    "                )\n",
    "        ],\n",
    "    },\n",
    "    # Maximum number of steps to take in the graph\n",
    "    {\"recursion_limit\": 150},\n",
    ")\n",
    "for s in events:\n",
    "    print(s)\n",
    "    print(\"----\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                  Firm Number_of_Funds Commitment Percent_of_Total_Comm  Exposure Percent_of_Total_Exposure TVPI Net_IRR\n",
      "         Spark Capital               -       13.3                     -         -                         -    -       -\n",
      "      Genstar Partners               -       50.0                     -         -                         -    -       -\n",
      " Gilde Buyout Partners               -      34.93                     -         -                         -    -       -\n",
      "      Harvest Partners               -       50.0                     -         -                         -    -       -\n",
      "                  DEFY               -       18.0                     -         -                         -    -       -\n",
      "         TA Associates               -       35.0                     -         -                         -    -       -\n",
      "  Advent International               5       45.0                   2.8 132320692                       3.7 1.64    16.0\n",
      "        Freeman Spogli               -       25.0                     -         -                         -    -       -\n",
      "       Platinum Equity               4       50.0                   2.2 105732861                       3.0 1.64    28.5\n",
      "                   NEA               4       35.0                   1.9 103565654                       2.9 1.60    15.7\n",
      "                   P4G               -       10.0                     -         -                         -    -       -\n",
      "             Oak HC-FT               -       25.0                     -         -                         -    -       -\n",
      "              Sunstone               -       10.0                     -         -                         -    -       -\n",
      "Montagu Private Equity               -      35.44                     -         -                         -    -       -\n",
      "                   KPS               -       40.0                     -         -                         -    -       -\n",
      "     Clearlake Capital               -       30.0                     -         -                         -    -       -\n"
     ]
    }
   ],
   "source": [
    "display_table()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
